# Copyright (c) 2022 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
from typing import Dict, Optional

import jinja2

from matter.idl.matter_idl_types import Idl

from .filters import RegisterCommonFilters
from .storage import GeneratorStorage

log = logging.getLogger(__name__)


class CodeGenerator:
    """
    Defines the general interface for things that can generate code output.

    A CodeGenerator takes a AST as input (a `Idl` type) and generates files
    as output (like java/cpp/mm/other).

    Its public interface surface is reasonably small:
      'storage' init argument specifies where generated code goes
      'idl' is the input AST to generate
      'render' will perform a rendering of all files.

    As special optimizations, CodeGenerators generally will try to read
    existing data and will not re-write content if not changed (so that
    write time of files do not change and rebuilds are not triggered).
    """

    def __init__(self, storage: GeneratorStorage, idl: Idl, loader: Optional[jinja2.BaseLoader] = None, fs_loader_searchpath: Optional[str] = None):
        """
        A code generator will render a parsed IDL (a AST) into a given storage.

        Args:
           storage: Storage to use to read/save data
           loader: if given, use a custom loader for templates
           fs_loader_searchpath: if a loader is NOT given, this controls the search path
              of a default FileSystemLoader that will be used
        """
        if not loader:
            if not fs_loader_searchpath:
                fs_loader_searchpath = os.path.dirname(__file__)
            loader = jinja2.FileSystemLoader(searchpath=fs_loader_searchpath)

        self.storage = storage
        self.idl = idl
        self.jinja_env = jinja2.Environment(
            loader=loader, keep_trailing_newline=True)
        self.dry_run = False

        RegisterCommonFilters(self.jinja_env.filters)

    def render(self, dry_run=False):
        """
        Renders  all required files given the idl contained in the code generator.
        Reset the list of generated outputs.

        Args:
          dry_run: if true, outputs are not actually written to disk.
                   if false, outputs are actually written to disk.
        """
        self.dry_run = dry_run
        self.internal_render_all()

    def internal_render_all(self):
        """This method is to be implemented by subclasses to run all generation
           as needed.
        """
        raise NotImplementedError("Method should be implemented by subclasses")

    def internal_render_one_output(self, template_path: str, output_file_name: str, vars: Dict):
        """
        Method to be called by subclasses to mark that a template is to be generated.

        File will either actually do a jinja2 generation or just log things
        if dry-run was requested during `render`.

        NOTE: to make this method suitable for rebuilds, this file will NOT alter
              the timestamp of the output file if the file content would not
              change (i.e. no write will be invoked in that case.)

        Args:
          template_path - the path to the template to be loaded for file generation.
                          Template MUST be a jinja2 template.
          output_file_name - File name that the template is to be generated to.
          vars             - variables used for template generation
        """
        log.info("File to be generated: '%s'", output_file_name)
        if self.dry_run:
            return

        log.info("Template path: '%s', CWD: '%s'", template_path, os.getcwd())
        rendered = self.jinja_env.get_template(template_path).render(vars)

        # Report regardless if it has changed or not. This is because even if
        # files are unchanged, validation of what the correct output is should
        # still be done.
        self.storage.report_output_file(output_file_name)

        if rendered == self.storage.get_existing_data(output_file_name):
            log.info("File content not changed")
        else:
            self.storage.write_new_data(output_file_name, rendered)
